基础知识-渲染管线

基本概念

Graphics Pipeline

Graphics Pipeline (图形渲染管线):管理 将3D坐标,转变为屏幕上的有色2D像素 的过程。也就是将一些原始图形数据,经过各种变化处理最终在屏幕上显示。

Graphics Pipeline 可以被划分为两个主要部分:

  1. 把3D坐标转换为2D坐标;
  2. 把2D坐标转变为实际的有颜色的像素。

Shader

Shader (着色器):运行在 GPU 的小程序,自定义显卡渲染画面的算法,使画面达到我们想要的效果。本质就是一段代码(主流的有 基于 OpenGL 的 GLSL、基于 DirectX 的 HLSL 等),这段代码的作用是阐述如何绘制每一个顶点的颜色以及最终每一个像素点的颜色。

渲染流程

我们可以将其分为三个阶段:应用阶段、几何阶段、光栅化阶段。

image-20211201192800788

应用阶段

CPU 负责。

image-20211201200709582

1. 准备场景数据

准备好 场景数据 ,包括:摄像机的位置、视锥体、场景中包含哪些模型、使用哪些光源等。

2. 粗粒度剔除

处理 场景数据, 将 在摄像机视锥体之外、被其他物品遮挡住 的物品剔除。

3. 设置渲染状态

渲染状态定义了场景中的一个个 Mesh 是如何处理的,例如:

  1. 设置渲染属性:使用哪个 Shader、Material、光源属性 来渲染、 动态静态合批GPU Instance 等;
  2. 设置渲染顺序:根据物体的 Render Queue 、距离相机的深度 等来设置渲染顺序;
  3. 设置渲染目标:将渲染输出到 屏幕 ,或者是 RenderTexture 等地方;
  4. 设置渲染模式:使用 前向渲染 ,或者是 延迟渲染 等。
4. 调用 Draw Call

将前面准备好的所有 渲染所需要的数据硬盘 加载到 系统内存 中,然后再加载到 显存 中,接着调用 Draw Call

Draw Call 就是由 CPU 发起、由 GPU 接收信息的命令。CPU 通过调用 Draw Call 来告诉 GPU 开始进行一个渲染流程,一个 Draw Call 会指向本次调用 需要渲染的图元列表

(图元是一组表示顶点位置的顶点,基本图元有:点、直线、三角形)

几何阶段

GPU 负责,以 顶点数据 作为输入,顶点数据是由 应用阶段Draw Call 指定的。

image-20211201200848484

1. 顶点着色器

进行顶点相关的一系列操作。通常用来实现顶点的 空间变换顶点着色 等操作。

它执行 通过矩阵变换位置(把顶点从 模型空间 变换到 齐次裁剪空间 )计算照明公式生成逐顶点颜色生成或变换纹理坐标 等基于顶点的操作。

2. 曲面细分着色器

用于 细分图元 ,将 Mesh 更加细分,获得更加高精度的模型。

比如:我们需要一个模型在远处比较粗糙,近看的时候更加精致(顶点数更多)。其中一种方案是使用 LOD ,但从渲染方面处理也可以使用 曲面细分着色器

3. 几何着色器

完整的图元 作为输入数据,可以执行 创建更多的图元销毁图元图元着色 等操作。

最简单的一个使用例子就是:比如我们可以将 一个图形 通过几何着色器扩展成 多个图形

4. 裁剪
  1. 对之前变换到 齐次裁剪空间 的图元操作,将不在摄像机视野范围内的部分剪裁掉;
  2. 剔除某些三角图元的面片,比如控制裁剪物体的正面或者背面。
5. 屏幕映射

把每个图元的坐标从 标准坐标系 (-1 ~ 1) 映射到 窗口坐标系 上。

光栅化阶段

image-20211201204219582

1. 三角形设置

上个阶段输出的图元是 三角网格的顶点 ,我们要得到整个三角形(不单单是顶点,还包括三角形内部)的覆盖情况,由顶点变为面,我们需要 三角形边界的表示方式

计算这样一个 三角网格的边界 的过程就是 三角形设置

2. 三角形遍历

得到 三角网格的边界 ,我们要计算得到 三角形内部的像素,如果被一个像素被这个三角形覆盖则生成 片元

每个片元中的信息就是三个顶点信息 插值 得到的。

片元并不是真正意义上的一个像素,它比像素的信息更多,包括了:屏幕坐标、深度信息这些 顶点着色器传递的数据

3. 逐片元操作 ①

三角形遍历 后,片元已经大致分出来了,接下来可以顺便进行 抗锯齿处理提前深度测试

image-20211201204330739

3.1 抗锯齿处理

抗锯齿就是为了处理 用有限离散的像素点去逼近连续的三角形出现的锯齿走样 现象。

这一步,我们可以执行 SSAA、MSAA 等支持 前向渲染 的算法来处理抗锯齿。FXAA / TAA 等算法用于后处理。

SSAA :超采样反走样(Super Sampling AA),将一个像素点再细分为多个采样点,计算每个采样点的颜色取均值,作为该像素点的颜色

MSAA :多采样反走样(Multi-Sampling AA),将一个像素点再细分为多个采样点,计算在三角面内的采样点占总采样点的比值,根据该比值计算该像素点的颜色。

3.2 提前深度测试

提前深度测试 也就是 Early-Z

要求片元着色器中 不能对深度进行修改 ,所以我们先判断片元着色器是否开启 透明度测试 (Alpha Test) ,如果没有的话进行 Early-Z

进行逐片元的深度测试和深度写入,将没通过该深度测试的片元舍弃。

4. 片元着色器

对片元进行纹理采样、颜色汇总等处理,计算一个像素的最终颜色。

5. 逐片元操作 ②

image-20211201205510462

5.1 透明度测试

将片元的 透明度 进行条件筛选,不满足条件的话(比如小于某个阈值),那么就舍弃该片元。

5.2 模板测试

将片元的 模板值 与缓冲区的模板值比较,(我们自己定义某个模板值与比较条件),不满足条件的话,舍弃该片元。

5.3 深度测试

将片元的 深度值 与深度缓冲区的值进行比较,不满足条件的话(片元深度更大),舍弃该片元。

5.4 颜色混合

将片元颜色与存在深度缓冲区中的颜色值进行混合。

OpenGL 绘制图形

Vertex

  1. 创建输入顶点数据
    输入Normalized Device Coordinates, NDC),也就是 -1.0 到 1.0 的值。

  2. 创建、绑定 VAO
    Vertex Array Object (顶点数组对象),顶点属性调用都会储存在 VAO 中。
    当配置顶点属性指针时,只需要将调用执行一次,之后再绘制物体的时候绑定相应的 VAO 即可。不同顶点数据和属性配置之间切换只需要绑定不同的VAO。

  3. 创建、绑定 VBO
    Vertex Buffer Object (顶点缓冲对象),通过 VBO 管理顶点数据储存在显卡的内存。
    绑定后缓冲调用都会用来配置当前绑定的缓冲。

  4. 创建、绑定 EBO
    Element Buffer Object (索引缓冲对象),专门储存索引,调用这些顶点的索引来决定该绘制哪个顶点。
    为了防止同一个点被多次渲染,我们用索引绘制(Indexed Drawing)来处理,把索引复制到缓冲里,使用当前绑定的索引缓冲对象中的索引进行绘制。

image-20210922211303724
  1. 链接顶点属性
    指定如何解析顶点数据(应用到逐个顶点属性上)。指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static float vertices0[] = {
-0.9f, 0.9f, 0.0f,
-0.8f, 0.8f, 0.0f,
-0.9f, 0.7f, 0.0f
};

static float vertices1[] = {
0.5f, 0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
-0.5f, 0.5f, 0.0f
};

static unsigned int indices1[] = {
0, 1, 3,
1, 2, 3
};

static unsigned int VAO[2], VBO[2], EBO;

static void InitializeVertex()
{
// Generate
glGenVertexArrays(2, VAO);
glGenBuffers(2, VBO);
glGenBuffers(1, &EBO);

// Bind 0
glBindVertexArray(VAO[0]);
glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices0), vertices0, GL_STATIC_DRAW);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// Bind 1
glBindVertexArray(VAO[1]);
glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices1), vertices1, GL_STATIC_DRAW);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices1), indices1, GL_STATIC_DRAW);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
}

Shader

  1. 创建、编译 Vertex Shader
    Vertex Shader (顶点着色器),处理顶点操作。
    GLSL (OpenGL Shading Language) 编写顶点着色器,然后编译这个着色器,这样我们就可以在程序中使用它。

  2. 创建、编译 Fragment Shader
    Fragment Shader (片元着色器),处理片元操作,计算像素最后的颜色输出。

  3. 链接着色器
    Shader Program Object 是多个着色器合并之后并最终链接完成的版本。
    如果要使用刚才编译的着色器我们必须把它们 链接 在一个 Shader 上,之后在渲染对象的时候激活这个 Shader。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
    之所以将这部放到最后,是因为我们创建、编译完成的一个 Vertex Shader 或一个 Fragment Shader 可以链接给多个不同的 Shader。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static unsigned int shaderProgram;

const char* vertexShaderSource =
"#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";

const char* fragmentShaderSource =
"#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);\n"
"}\0";

static void InitializeShader()
{
shaderProgram = glCreateProgram();

unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

// link shaders
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
}

完整代码

以输出 一个三角形 和 一个矩形(两个三角形组成)为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#define GLEW_STATIC
#include <GL/glew.h>
#include <GL/GL.h>
#include <GLFW/glfw3.h>

#include <iostream>

#pragma region Setting

static GLFWwindow* window;
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

static void InitializeWindow()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Test", NULL, NULL);
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, [](GLFWwindow* window, int width, int height){ glViewport(0, 0, width, height); });


glewExperimental = GL_TRUE;
glewInit();

}

static void ProcessInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
#pragma endregion


#pragma region InitializeVertex

static float vertices0[] = {
-0.9f, 0.9f, 0.0f,
-0.8f, 0.8f, 0.0f,
-0.9f, 0.7f, 0.0f
};

static float vertices1[] = {
0.5f, 0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
-0.5f, 0.5f, 0.0f
};

static unsigned int indices1[] = {
0, 1, 3,
1, 2, 3
};

static unsigned int VAO[2], VBO[2], EBO;

static void InitializeVertex()
{
// Generate
glGenVertexArrays(2, VAO);
glGenBuffers(2, VBO);
glGenBuffers(1, &EBO);

// Bind 0
glBindVertexArray(VAO[0]);
glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices0), vertices0, GL_STATIC_DRAW);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// Bind 1
glBindVertexArray(VAO[1]);
glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices1), vertices1, GL_STATIC_DRAW);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices1), indices1, GL_STATIC_DRAW);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
}

#pragma endregion


#pragma region InitializeShader

static unsigned int shaderProgram;

const char* vertexShaderSource =
"#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";

const char* fragmentShaderSource =
"#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);\n"
"}\0";

static void InitializeShader()
{
shaderProgram = glCreateProgram();

unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

// link shaders
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
}

#pragma endregion

void Render()
{
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

// 0
glUseProgram(shaderProgram);
glBindVertexArray(VAO[0]);
glDrawArrays(GL_TRIANGLES, 0, 3);

// 1
glUseProgram(shaderProgram);
glBindVertexArray(VAO[1]);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}

int main()
{
InitializeWindow();
InitializeVertex();
InitializeShader();

while (!glfwWindowShouldClose(window))
{
ProcessInput(window);

Render();

glfwSwapBuffers(window);
glfwPollEvents();
}

glfwTerminate();
return 0;
}